<template>
    <div class="upload-container">
        <div>
            <div style="display:none;">
                <video width="500" height="240" controls id="up-video">
                </video>
            </div>
            <h2 class="forbid-select">{{ '上传至' + bucket }}</h2>
            <el-upload ref="upload" action="" :show-file-list="false" :auto-upload="false" multiple
                :on-remove="handleRemove" :on-change="handleFileChange" :file-list="uploadFileList" class="upload">
                <template #trigger>
                    <el-button class="fit-auto-resize" type="primary">选择文件</el-button>
                </template>
                <el-button class="fit-auto-resize" type="success" @click="handler" :disabled="disableUpload">
                    上传
                </el-button>
                <el-button type="danger" class="fit-auto-resize" @click="clearFileHandler" :disabled="disableClear">
                    清空
                </el-button>
            </el-upload>
            <el-row style="margin-top: 20px!important;padding-bottom: 5px;width: 100%;border-bottom: 1px solid #e4e7ec;"
                onselectstart="return false" class="forbid-select">
                <el-col :span="8" class="file-title font-auto-resize">
                    文件名
                </el-col>
                <el-col :span="4" class="file-title font-auto-resize">
                    文件大小
                </el-col>
                <el-col :span="6" class="file-title font-auto-resize">
                    上传进度
                </el-col>
                <el-col :span="6" class="file-title font-auto-resize">
                    状态
                </el-col>
            </el-row>
            <!-- 文件列表 -->
            <div style="height: calc(100vh - 135px)">
                <el-scrollbar style="width: 100%;height: 100%">
                    <div class="file-list-wrapper">
                        <el-collapse>
                            <el-collapse-item v-for="(item, index) in uploadFileList" :key="index"
                                :disabled="item.status === '等待上传'"
                                @dblclick.n.native.prevent.stop="clickRemove(index, item)">
                                <template #title>
                                    <el-row class="upload-file-item forbid-select" onselectstart="return false">
                                        <el-col :span="8">
                                            <div class="file-info-item file-name font-auto-resize">
                                                {{ item.name }}
                                            </div>
                                        </el-col>
                                        <el-col :span="4">
                                            <div class="file-info-item file-size font-auto-resize">{{
                                                transformByte(item.size) }}</div>
                                        </el-col>
                                        <el-col :span="6">
                                            <div class="file-info-item file-progress">
                                                <div style="margin: auto">
                                                    <span class="file-progress-label"></span>
                                                    <el-progress class="file-progress-value" :text-inside="true"
                                                        :percentage="item.status === '成功' ? 100 : item.uploadProgress"
                                                        :stroke-width="strokeWidth"
                                                        :status="item.uploadProgress === 100 ? 'success' : item.progressStatus" />
                                                </div>
                                            </div>
                                        </el-col>
                                        <el-col :span="6">
                                            <div class="file-info-item file-size">
                                                <el-tag class="upload-status input-auto-resize"
                                                    :type="getTagType(item.status)" disable-transitions>
                                                    {{ item.status }}
                                                </el-tag>
                                            </div>
                                        </el-col>
                                    </el-row>
                                </template>
                                <div class="file-chunk-list-wrapper">
                                    <!-- 分片列表 -->
                                    <el-table :data="item.chunkList" max-height="500" style="width: 100%"
                                        :header-cell-style="{ 'text-align': 'center' }" v-if="!item.checkResult"
                                        :cell-style="{ 'padding': '2px', 'text-align': 'center' }">
                                        <el-table-column prop="chunkNumber" label="分片序号" width="180" />
                                        <el-table-column prop="progress" label="上传进度">
                                            <template v-slot="{ row }">
                                                <el-progress :percentage="row.progress"
                                                    :status="row.progress === 100 ? 'success' : row.progressStatus"
                                                    :text-inside="true" :stroke-width="strokeWidth" />
                                            </template>
                                        </el-table-column>
                                        <el-table-column prop="status" label="状态" width="180" />
                                    </el-table>
                                    <div v-else style="padding-left: 5px; font-size: 13px;">
                                        <span onselectstart="return false" class="forbid-select">
                                            此文件在该文件夹下已存在，文件名：
                                        </span>
                                        <span style="color: red">
                                            {{ item.checkResult.fileName }}
                                        </span>
                                        <span onselectstart="return false" class="forbid-select">
                                            ，MD5：
                                        </span>
                                        <span style="color: red">
                                            {{ item.checkResult.fileMd5 }}
                                        </span>
                                    </div>
                                </div>
                            </el-collapse-item>
                        </el-collapse>
                    </div>
                </el-scrollbar>
            </div>
        </div>
    </div>
</template>
<script setup>
import { useElMessage, useRequest } from "@usehook"
import { fileSuffixTypeUtil } from "@/utils/fileUtil"
import axios from 'axios'
import SparkMD5 from 'spark-md5'
import { ArrayRemove } from "@/utils/tools"
import { config } from "@/utils/setting"

const checkUpload = (md5, filename, bucket) => {
    return useRequest({ url: `/file/multipart/check?md5=${md5}&filename=${filename}&bucket=${bucket}` })
}

const strokeWidth = $computed(() => {
    return config.value.fontSize - 4
})

const props = defineProps({
    bucket: {
        type: String,
        required: true
    }
})
const fileUploadId = 'file_upload_id'
const chunkSize = 10 * 1024 * 1024 //10mb
let currentFileIndex = $ref(0)
const FileStatus = {
    wait: '等待上传',
    getMd5: '校验MD5',
    chip: '正在创建序列',
    chipFailed: '创建序列失败',
    uploading: '正在上传',
    success: '上传成功',
    error: '上传失败',
    merge: '正在合并文件',
    mergeFailed: "合并文件失败",
    exist: "文件已存在"
}

let checkFileCharset = $ref('')

// 上传并发数
let simultaneousUploads = 3
let uploadFileList = $ref([])
let abortController = new AbortController()

const clickRemove = (index, item) => {
    if (item.status !== FileStatus.wait) return
    uploadFileList.splice(index, 1)
    if (currentFileIndex > uploadFileList.length) currentFileIndex = uploadFileList.length
}

const getTagType = (status) => {
    if (status === FileStatus.error || status === FileStatus.mergeFailed || status === FileStatus.chipFailed) return "danger"
    if (status === FileStatus.wait) return "info"
    if (status === FileStatus.success || status === FileStatus.exist) return "success"
    return "warning"
}

const uploading = computed(() => uploadFileList.some(e => [FileStatus.uploading, FileStatus.getMd5, FileStatus.chip, FileStatus.merge].includes(e.status)))
const disableUpload = computed(() => uploadFileList.length === 0 || currentFileIndex >= uploadFileList.length || uploading.value)
const disableClear = computed(() => uploadFileList.length === 0 || uploading.value)

// 上传文件
const handler = () => {
    //判断文件列表是否为空
    if (uploadFileList.length === 0) {
        useElMessage.error('请先选择文件')
        return
    }

    if (currentFileIndex >= uploadFileList.length) {
        useElMessage.success("没有新的文件需要上传")
        return
    }
    //当前操作文件
    const currentFile = uploadFileList[currentFileIndex]
    //更新上传标签
    currentFile.status = FileStatus.getMd5
    currentFile.chunkUploadedList = []

    // 1. 计算文件MD5
    getFileMd5(currentFile.raw, async (md5, totalChunks, filename, bucket) => {
        // 2. 检查是否已上传
        const checkResult = await checkFileUploaded(md5, filename, bucket)
        if (!checkResult) {
            currentFile.status = FileStatus.error
            handlerNext()
            return
        }
        if (checkResult.responseCode === 900) {
            currentFile.status = FileStatus.exist
            currentFile.uploadProgress = 100
            currentFile.checkResult = checkResult.data
            //如果此文件上传过，就跳到下一个文件
            handlerNext()
            return
        } else if (checkResult.responseCode === 901) {  // "上传中" 状态    后台暂无此部分逻辑处理
            // 获取已上传分片列表
            currentFile.status = FileStatus.uploading
            currentFile.chunkUploadedList = checkResult.data.chunkUploadedList
        }
        // 3. 正在创建分片
        currentFile.status = FileStatus.chip

        //创建分片
        let fileChunks = createFileChunk(currentFile.raw, chunkSize)

        // 获取文件类型
        let type = fileSuffixTypeUtil(currentFile.name)

        let param = {
            fileName: currentFile.name,
            fileSize: currentFile.size,
            chunkSize: chunkSize,
            chunkNum: totalChunks,
            fileMd5: md5,
            contentType: 'application/octet-stream',
            fileType: type,
            bucketName: props.bucket,
            chunkUploadedList: currentFile.chunkUploadedList//已上传的分片索引+1
        }

        // 4. 获取上传url
        let uploadIdInfoResult = await getFileUploadUrls(param)
        if (!uploadIdInfoResult.status) {
            currentFile.status = FileStatus.chipFailed
            //递归上传下一个文件
            handlerNext()
            return
        }

        let uploadIdInfo = uploadIdInfoResult.data

        let uploadUrls = uploadIdInfo.urlList

        currentFile.chunkList = []

        if (uploadUrls !== undefined) {
            if (fileChunks.length !== uploadUrls.length) {
                useElMessage.error('文件分片上传地址获取错误')
                return
            }
        }

        fileChunks.map((chunkItem, index) => {
            if (currentFile.chunkUploadedList.indexOf(index + 1) !== -1) {
                currentFile.chunkList.push({
                    chunkNumber: index + 1,
                    chunk: chunkItem,
                    uploadUrl: uploadUrls[index],
                    progress: 100,
                    progressStatus: 'success',
                    status: '上传成功'
                })
            } else {
                currentFile.chunkList.push({
                    chunkNumber: index + 1,
                    chunk: chunkItem,
                    uploadUrl: uploadUrls[index],
                    progress: 0,
                    status: '—'
                })
            }
        })

        let tempFileChunks = []

        currentFile.chunkList.forEach((item) => {
            tempFileChunks.push(item)
        })

        //更新状态
        currentFile.status = FileStatus.uploading

        // 处理分片列表，删除已上传的分片
        tempFileChunks = processUploadChunkList(tempFileChunks)

        // 5. 上传
        try {
            await uploadChunkBase(tempFileChunks)
        } catch (e) {
            currentFile.status = FileStatus.error
            //递归上传下一个文件
            handlerNext()
            return
        }

        //判断是否单文件上传或者分片上传
        if (uploadIdInfo.uploadId === "SingleFileUpload") {
            setCurrentFileSuccess()
            useRequest({
                url: "/file/saveFileInfo", method: 'post',
                uploadId: uploadIdInfo.uploadId,
                fileName: currentFile.name,
                fileMd5: md5,
                fileType: type,
                chunkNum: uploadIdInfo.urlList.length,
                chunkSize: chunkSize,
                fileSize: currentFile.size,
                bucketName: props.bucket
            }).then(e => {
                console.log(e)
            })
            //递归上传下一个文件
            handlerNext()
        } else {
            currentFile.status = FileStatus.merge
            // 6. 合并文件
            const mergeResult = await mergeFile({
                uploadId: uploadIdInfo.uploadId,
                fileName: currentFile.name,
                fileMd5: md5,
                fileType: type,
                chunkNum: uploadIdInfo.urlList.length,
                chunkSize: chunkSize,
                fileSize: currentFile.size,
                bucketName: props.bucket
            })

            //合并文件状态
            if (!mergeResult.status) {
                currentFile.status = FileStatus.mergeFailed
            } else {
                localStorage.removeItem(fileUploadId)
                currentFile.status = FileStatus.success
            }
            //递归上传下一个文件
            handlerNext()
        }
    })
}

const setCurrentFileSuccess = () => {
    const currentFile = uploadFileList[currentFileIndex]
    if (!currentFile || !currentFile.chunkList) {
        return
    }
    // 计算方式：已上传大小 / 文件总大小
    currentFile.uploadProgress = 100
    //更新状态
    currentFile.status = FileStatus.success
    uploadFileList[currentFileIndex] = currentFile
}

const handlerNext = () => {
    currentFileIndex++
    //文件下标偏移
    if (currentFileIndex < uploadFileList.length) {
        setTimeout(() => {
            handler()
        }, 500)
    }
}

const clearFileHandler = () => {
    uploadFileList = []
    // fileIndex = 0
    currentFileIndex = 0
}

const abortUploadRequest = () => {
    abortController.abort()
    abortController = new AbortController()
}

const checkUploadComplete = () => {
    return currentFileIndex === uploadFileList.length
}

const checkFileHandler = () => {
    if (uploadFileList.length === 0) clearFileHandler()
}

/**
* 上传文件列表
* @param {*} file
* @param {*} fileList
*/
const handleFileChange = (file, fileList) => {
    for (let c of checkFileCharset.split('')) {
        if (file.name.includes(c)) {
            useElMessage.error("文件" + file.name + "存在[" + c + "]字符，请修改后重新上传")
            ArrayRemove(file, fileList)
            return
        }
    }
    initFileProperties(file)
    uploadFileList = fileList
}

//初始化文件属性
const initFileProperties = (file) => {
    file.chunkList = []
    file.status = FileStatus.wait
    file.progressStatus = ''
    file.uploadProgress = 0
}

/**
* 移除文件列表
* @param {*} file
* @param {*} fileList
*/
const handleRemove = (file, fileList) => {
    uploadFileList = fileList
}

/**
* 分片读取文件 获取文件的MD5
* @param file
* @param callback
*/
const getFileMd5 = (file, callback) => {
    const blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice
    const fileReader = new FileReader()
    // 计算分片数
    const totalChunks = Math.ceil(file.size / chunkSize)
    let currentChunk = 0
    const spark = new SparkMD5.ArrayBuffer()
    loadNext()
    fileReader.onload = (e) => {
        try {
            spark.append(e.target.result)
        } catch (error) {
            console.log('获取Md5错误：' + currentChunk)
        }
        if (currentChunk < totalChunks) {
            currentChunk++
            loadNext()
        } else {
            callback(spark.end(), totalChunks, file.name, props.bucket)
        }
    }
    fileReader.onerror = function () {
        console.warn('读取Md5失败，文件读取错误')
    }

    function loadNext() {
        const start = currentChunk * chunkSize
        const end = ((start + chunkSize) >= file.size) ? file.size : start + chunkSize
        // 注意这里的 fileRaw
        fileReader.readAsArrayBuffer(blobSlice.call(file, start, end))
    }
}

/**
* 文件分片
*/
const createFileChunk = (file, size = chunkSize) => {
    const fileChunkList = []
    let count = 0
    while (count < file.size) {
        fileChunkList.push({
            file: file.slice(count, count + size),
        })
        count += size
    }
    return fileChunkList
}

/**
* 处理即将上传的分片列表，判断是否有已上传的分片，有则从列表中删除
*/
const processUploadChunkList = (chunkList) => {
    const currentFile = uploadFileList[currentFileIndex]
    let chunkUploadedList = currentFile.chunkUploadedList
    if (chunkUploadedList === undefined || chunkUploadedList === null || chunkUploadedList.length === 0) {
        return chunkList
    }
    //
    for (let i = chunkList.length - 1; i >= 0; i--) {
        const chunkItem = chunkList[i]
        for (let j = 0; j < chunkUploadedList.length; j++) {
            if (chunkItem.chunkNumber === chunkUploadedList[j]) {
                chunkList.splice(i, 1)
                break
            }
        }
    }
    return chunkList
}

const updateChunkErrorStatus = (number) => {
    const chunkList = uploadFileList[currentFileIndex].chunkList
    let error = false
    for (let chunk of chunkList) {
        if (chunk.chunkNumber === number) {
            chunk.status = '上传失败'
            chunk.progress = 0
            error = true
        }
    }

    if (error) {
        uploadFileList[currentFileIndex].status = FileStatus.error
    }
    getCurrentFileProgress()
}

/**
* 上传分片
* @param chunkList
* @returns {Promise<unknown>}
*/
const uploadChunkBase = (chunkList) => {
    let successCount = 0
    let totalChunks = chunkList.length
    let errorCount = 0
    let totalCount = 0
    return new Promise((resolve, reject) => {
        const handler = () => {
            if (chunkList.length) {
                const chunkItem = chunkList.shift()
                // 直接上传二进制，不需要构造 FormData，否则上传后文件损坏
                axios.put(chunkItem.uploadUrl, chunkItem.chunk.file, {
                    // 上传进度处理
                    onUploadProgress: checkChunkUploadProgress(chunkItem),
                    headers: {
                        'Content-Type': 'application/octet-stream'
                    },
                    signal: abortController.signal
                }).then(response => {
                    if (response.status === 200) {
                        successCount++
                        totalCount++
                        console.log('上传成功：' + JSON.stringify(chunkItem))
                        chunkItem.progress = 100
                        chunkItem.status = FileStatus.success
                        // 继续上传下一个分片
                        //  debugger
                        handler()
                    } else {
                        errorCount++
                        totalCount++
                        console.log('上传失败：' + response.status + '，' + response.statusText)
                        updateChunkErrorStatus(chunkItem.chunkNumber)
                        handler()
                    }
                }).catch(error => {
                    // 更新状态
                    console.log('分片：' + chunkItem.chunkNumber + ' 上传失败，' + error)
                    updateChunkErrorStatus(chunkItem.chunkNumber)
                    errorCount++
                    totalCount++

                    // 重新添加到队列中
                    //chunkList.push(chunkItem)
                    handler()
                })
            }

            if (successCount >= totalChunks) {
                resolve()
            } else if (totalCount >= totalChunks) {
                reject("上传失败")
            }
        }
        // 并发
        for (let i = 0; i < simultaneousUploads; i++) {
            handler()
        }
    })
}

/**
* 获取直接上传的uri链接
*/
const getFileUploadUrls = (fileParam) => {
    return useRequest({ url: "/file/multipart/init", method: 'post', ...fileParam })
}

/**
* 根据MD5查看文件是否上传过
*/
const checkFileUploaded = (md5, filename, bucket) => {
    return new Promise((resolve, reject) => {
        checkUpload(md5, filename, bucket).then(response => {
            resolve(response)
        }).catch(error => {
            reject(error)
        })
    })
}

/**
* 合并文件
*/
const mergeFile = (fileParam) => {
    return new Promise((resolve) => {
        useRequest({ url: "/file/multipart/merge", method: "post", ...fileParam }).then(response => {
            let data = response
            if (!data.status) {
                data.message = FileStatus.mergeFailed
                resolve(data)
            } else {
                data.message = FileStatus.success
                resolve(data)
            }
        }).catch(error => {
            console.log(error)
            resolve({ status: false })
        })
    })
}

/**
* 检查分片上传进度
*/
const checkChunkUploadProgress = (item) => {
    return p => {
        const currentFile = uploadFileList[currentFileIndex]
        item.progress = item.progress === 100 || currentFile.status === FileStatus.success ? 100 : parseInt(String((p.loaded / p.total) * 100))
        updateChunkUploadStatus(item)
    }
}

const updateChunkUploadStatus = (item) => {
    let status = FileStatus.uploading
    let progressStatus = ''
    if (item.progress >= 100) {
        status = FileStatus.success
        progressStatus = 'success'
    }
    let chunkIndex = item.chunkNumber - 1
    let currentChunk = uploadFileList[currentFileIndex]?.chunkList[chunkIndex]
    if (!currentChunk) return;
    // 修改状态
    currentChunk.status = status
    currentChunk.progressStatus = progressStatus
    // 更新状态
    uploadFileList[currentFileIndex].chunkList[chunkIndex] = currentChunk
    // 获取文件上传进度
    getCurrentFileProgress()
}

const getCurrentFileProgress = () => {
    const currentFile = uploadFileList[currentFileIndex]
    if (!currentFile || !currentFile.chunkList) return
    const chunkList = currentFile.chunkList
    const uploadedSize = chunkList.map((item) => item.chunk.file.size * item.progress).reduce((acc, cur) => acc + cur)
    // 计算方式：已上传大小 / 文件总大小
    currentFile.uploadProgress = parseInt((uploadedSize / currentFile.size).toFixed(2))
    uploadFileList[currentFileIndex] = currentFile
}

const transformByte = (size) => {
    if (!size) return '0B'
    const unitSize = 1024
    if (size < unitSize) return size + ' B'
    // KB
    if (size < Math.pow(unitSize, 2)) return (size / unitSize).toFixed(2) + ' K'
    // MB
    if (size < Math.pow(unitSize, 3)) return (size / Math.pow(unitSize, 2)).toFixed(2) + ' MB'
    // GB
    if (size < Math.pow(unitSize, 4)) return (size / Math.pow(unitSize, 3)).toFixed(2) + ' GB'
    // TB
    return (size / Math.pow(unitSize, 4)).toFixed(2) + ' TB'
}

onMounted(() => {
    useRequest({ url: "/system/getSetting", classCode: "minio", name: "reject_char" }).then(e => {
        checkFileCharset = e.data
    })
})

defineExpose({ checkUploadComplete, clearFileHandler, abortUploadRequest })
</script>

<style scoped>
.upload-container {
    width: 100%;
    margin: 0 auto;
}

:deep(.file-list-wrapper) {
    margin-top: 3px;
}

:deep(h2) {
    text-align: center;
    margin: 10px;
}

:deep(.upload-file-item) {
    width: 100%;
}

:deep(.file-progress) {
    display: flex;
    align-items: center;
    height: 48px;
    margin-left: 15px;
    width: 100%;
}

:deep(.file-progress-value) {
    width: 150px;
}

:deep(.file-name) {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    text-align: center;
}

:deep(.file-size) {
    text-align: center;
}

:deep(.el-collapse-item) {
    margin-bottom: 5px;
}

:deep(.el-collapse-item__content) {
    padding-bottom: 0;
}

:deep(.el-collapse-item__wrap) {
    border-bottom: 0;
    margin-top: 1px;
}

:deep(.el-collapse-item__header) {
    background: #efefef;
    border-radius: 4px;
}

:deep(.el-collapse) {
    border: 0;
}

:deep(.el-table__header-wrapper) {
    border-radius: 4px;
}

:deep(.el-progress-bar__outer) {
    background: #d2dcf3;
}

:deep(.el-upload),
:deep(.el-upload-dragger) {
    padding-left: 10px;
    padding-right: 5px;
}

:deep(.file-title) {
    display: inline-block;
    font-size: 12px;
    color: #909399;
    text-align: center;
}

:deep(.el-progress-bar__innerText) {
    display: block;
    font-size: calc(var(--font-size) - 4px);
}

:deep(.upload-status) {
    margin-left: 40px;
    width: 140px;
}

:deep(.el-collapse-item__header.focusing:focus:not(:hover)) {
    color: #303133;
}

:deep(.el-scrollbar__bar.is-vertical) {
    display: none;
}

:deep(.el-collapse-item.is-disabled .el-collapse-item__header) {
    cursor: default;
}

:deep(.file-info-item) {
    height: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
}

:deep(.upload) {
    display: flex;
    align-items: center;
}
</style>
